package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CatchClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author HSJ
 * @since 2023-02-22
 */
@Service
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private CatchClient catchClient;

    @Override
    public Result queryById(Long id) {

//        log.debug(" 空值写入redis 和 设置过期时间  解决  缓存穿透 ");
//        Shop shop = queryWithPassThrough(id);


//       log.debug("互斥锁 解决  缓存击穿");
//       Shop shop = queryWithMutex(id);

//        log.debug("逻辑过期 解决 缓存击穿");
//        Shop shop = queryWithLogicalExpire(id);

    /** 推荐 使用 自己封装的 操作redis的工具类
     **
     */
//        log.debug("使用封装的 返回""空字符串的 方法  解决缓存穿透的问题");
//        Shop shop = catchClient.queryWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//

        log.debug("使用封装的 逻辑过期+锁的 方法 解决缓存击穿的问题");
        Shop shop = catchClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);


        return Result.ok(shop);

    }

    @Override
    @Transactional
    // TODO 单体式的项目，可以直接通过事务，控制更新数据库和删除缓存的原子性操作， 但如果分布式项目，采用TCC方案，用MQ通知其他服务删除缓存
    public Result update(Shop shop) {
        log.debug(">>>>>>>>> 更新 店铺 shop信息");
        Long id = shop.getId();
        if (id == null) {
            log.debug("店铺id为null，直接返回失败 <<<<<<<<");
            return Result.fail("店铺 id 不能为空");
        }
        log.debug("更新数据库");
        updateById(shop);
        log.debug("删除缓存");
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
        log.debug("更新数据库并删除缓存完成<<<<<<<<<<");
        return Result.ok();
    }

    /**
     * 过期时间 + 写入redis ""空字符串   解决          缓存穿透
     *
     * @param id
     * @return
     */
    public Shop queryWithPassThrough(Long id) {
        log.debug(">>>>>>根据id查询商铺信息 ");
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        log.debug("先从redis 查询商铺");
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        log.debug("判断 redis中是否存在 该商铺信息");
        if (StrUtil.isNotBlank(shopJson)) {
            log.debug("redis有 id={}的商铺信息,直接返回<<<<<<<<<<<<<", id);
            return JSONUtil.toBean(shopJson, Shop.class);

        }
        if (shopJson != null) {
            log.debug("redis里 获取到的 商铺信息为 空字符串  说明是为了防止缓存穿透,不在查数据库 直接返回 错误信息即可 <<<<<<<<<");
            return null;
        }
        log.debug("redis没有 id={}商铺信息,查询数据库", id);
        Shop shop = getById(id);
        if (shop == null) {
            log.debug("数据库没查到 id={}商铺信息 <<<<<<<<<", id);
            log.debug("将 空值  写入 redis 设置短暂的过期时间  防止缓存穿透");
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        log.debug("数据库查到 id={}商铺信息, 写入 redis,并设置过期时间 目的定时更新数据，保证和数据库的一致性", id);
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        log.debug("返回前端 shop信息 <<<<<<");
        return null;
    }

    /**
     * 互斥锁            解决      缓存击穿
     *
     * @param id
     * @return
     */

    private Shop queryWithMutex(Long id) {
        log.debug(">>>>>>根据id查询商铺信息 ");
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        log.debug("先从redis 查询商铺");
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        log.debug("判断 redis中是否存在 该商铺信息");
        if (StrUtil.isNotBlank(shopJson)) {
            log.debug("redis存在 id={}的商铺信息,直接返回<<<<<<<<<<<<<", id);
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        if (shopJson != null) {
            log.debug("redis里 获取到的 商铺信息为 空字符串  说明是为了防止缓存穿透,不在查数据库 直接返回 错误信息即可 <<<<<<<<<");
            return null;
        }
        log.debug("！！实现 缓存 重建");
        log.debug("获取互斥锁");
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            log.debug("获取锁失败，休眠 并 重试");
            if (!isLock) {
                Thread.sleep(50);
                //重试，在此处就是 递归
                return queryWithMutex(id);
            }
            log.debug("获取锁成功，根据id查询数据库，将数据写入redis");
            shop = getById(id);
            if (shop == null) {
                log.debug("数据库 没有id数据，将空字符串插入redis，防止缓存穿透");
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            log.debug("释放互斥锁");
            unlock(lockKey);
        }
        log.debug("返回shop");
        return shop;
    }

    // 解决缓存击穿、 加锁！ 拿到锁的线程去拼装id，并加锁，其他等待
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);//如果直接返回 可能会做拆箱、拆箱过程中可能会空指针
    }

    //释放锁
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }


    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 逻辑 过期   解决  缓存 击穿
     *
     * @return
     */
    private Shop queryWithLogicalExpire(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        log.debug("redis 获取 key的值");
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(shopJson)) {
            log.debug("redis不存在key，直接返回shop");
            return null;
        }
        log.debug("redis存在key，判断是否过期");
        log.debug("获取redis存放的 RedisData的 expireTime");
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            log.debug("未过期， 返回店铺信息");
            return shop;
        }
        log.debug("过期，重建  缓存");
        log.debug("获取互斥锁");
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        log.debug("判断是否 获取到锁");
        if (isLock) {
            try {
                log.debug("获取到锁, 开启独立线程 去做 缓存重建");
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    this.saveShop2Redis(id, 20L);
                });
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unlock(lockKey);
            }
        }
        log.debug("没有获取到锁，直接将redis中过期的数据返回");
        return shop;
    }


    /**
     * 缓存重建 逻辑
     *
     * @param id            店铺id
     * @param expireSeconds 逻辑过期 时间
     */
    public void saveShop2Redis(Long id, Long expireSeconds) {
        log.debug("查询店铺数据");
        Shop shop = getById(id);
        log.debug("封装逻辑国企使劲啊");
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        log.debug("写入 redis");
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }
}
